from Tkinter import *
import random
import pyaudio
import aifc
import math
import audioop
import struct
import wave
import threading

##def makeBeep(freq=440,dur=s,width=w,rate=s):
##    vals=((2**8)**width)/2
##
##    buf=[]
##    for i in range(dur):
##        v=int(vals*math.sin(i*2*math.pi*freq/rate)) 
##        buf.append(v)
##
##    return struct.pack("%sh" %int(dur), *buf)

class BigApp(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)
        self.pack()

        self.tempo=120
        
        self.eTempo = Entry()
        self.eTempo.pack()
        self.vTempo = StringVar()
        self.vTempo.set(str(self.tempo))
        self.eTempo["textvariable"] = self.vTempo
        self.eTempo.bind("<Key-Return>",self.doTempo)  

        self.tracks=["mj","amen","spies"]
        self.frames=[]
        for i in self.tracks:
            self.frames.append(App(i,self.tempo))

    def doTempo(self,event):
        try:
            if float(self.vTempo.get())<=0:    
                self.vTempo.set(str(self.tempo))                
        except:
            self.vTempo.set(str(self.tempo))
        self.tempo=float(self.vTempo.get())
        for i in self.frames:
            i.setTempo(self.tempo,event)
        
class App(Frame):
    def destroyH(self,event):
        self.stop()
        self.stream.close()
        self.p.terminate()
        print "stream closed"

    def setTempo(self,tempo,event=[]):
        newFact= tempo/(60.0*self.steps/(float(self.vEnd.get())-float(self.vStart.get())))
        self.vFactor.set(str(newFact))
        self.doFactor(event)

    def doFactor(self,event=[]):
        if event!=[]:
            self.stop()
            self.stream.close()

        try:
            if float(self.vFactor.get())<=0.0 or float(self.vFactor.get())*self.rate>192000:
                self.vFactor.set(str(self.factor))
        except:
                self.vFactor.set(str(self.factor))
        self.factor=float(self.vFactor.get())
        
        self.stream = self.p.open(format = self.p.get_format_from_width(self.width),channels = self.channels,rate = int(self.rate*self.factor),output = True)
        print "stream open"
        if event!=[]:
            self.play()
    
    def getAudioData(self):
        t=[]
        self.steps=16
        if self.track=="mj":
            f=aifc.open("wannaBeStartinSomethin.aif")
            t=(.71, 32.05)
            self.steps=64
        elif self.track=="spies":
            f=wave.open("broke_it.wav")
            self.steps=47
        elif self.track=="amen":
            f=wave.open("amen_1.wav")
            #already cut correctly
            self.steps=16
        else: raise Exception("unrecognized track")
        
        n=f.getnframes()
        w=f.getsampwidth()
        c=f.getnchannels()
        s=f.getframerate()
        print f.getcomptype(),f.getcompname()

        data=f.readframes(n)
        f.close()
        print "data loaded"

        self.fileDur=1.0*n/s

        if t==[]:
            t=(0.0,self.fileDur)

        self.initStart,self.initEnd=t
        self.rate=s      
            
        if c>1:
            data=audioop.tomono(data,w,.5,.5)
            c=1

        if isinstance(f,aifc.Aifc_read) and w==2:
            self.data=struct.unpack(">%sh" %n,data) #> is for big-endian
        elif isinstance(f,wave.Wave_read) and w==2:
            self.data=struct.unpack("%sh" %n,data)
        elif isinstance(f,wave.Wave_read) and w==1:
            data = audioop.bias(data, w, 128) #see doc for lin2lin -- has to do with 1-byte samples being unsigned, unlike 2- and 4-byters
            data=audioop.lin2lin(data,w,2) #have to do this cuz unpack not supporting B
            w=2
            self.data=struct.unpack("%sh" %n,data) #"b" and "B" unsupported (should be B), "c" gives: OverflowError: unsigned byte integer is greater than maximum
        else:
            print f,w
            raise Exception("unrecognized format")
        
        #for i in range(numSamps):
        #    print audioop.getsample(data,w,i),
        print "data converted"

        def ifElse(x,y,z):
            if x: return y
            else: return z

        def shift(x):
            return x[1:len(x)]+(x[len(x)-1],)

        def shiftList(x):
            return x[1:len(x)]+[x[len(x)-1]]

        def findUpwardCrossings(x):
            y=map(lambda a,b,c:ifElse(a>=0 and b<=0,c,-1),shift(x),x,range(len(x)))
            return filter(lambda a:a>-1,y)
            
        self.zeros=findUpwardCrossings(self.data)
        print "upward crossings cached"

        self.width=w
        self.channels=c
        self.p = pyaudio.PyAudio()

    def __init__(self, track, tempo, master=None):
        self.track=track
        
        self.getAudioData()
        
        self.orderStr="range(self.steps)"
        self.order=eval(self.orderStr)
        
        Frame.__init__(self, master)
        self.pack()
        self.bind("<Destroy>",self.destroyH) #might be dangerous to wait this long to set this up

        self.vOn = IntVar()
        self.vOn.set(1)
        self.cOn = Checkbutton(command=self.doOn,text=self.track,variable=self.vOn)
        self.cOn.pack()
        
        self.eStart = Entry()
        self.eStart.pack()
        self.vStart = StringVar()
        self.vStart.set(str(self.initStart))
        self.eStart["textvariable"] = self.vStart
        self.eStart.bind("<Key-Return>",self.doStart)        

        self.eEnd = Entry()
        self.eEnd.pack()
        self.vEnd = StringVar()
        self.vEnd.set(str(self.initEnd))
        self.eEnd["textvariable"] = self.vEnd
        self.eEnd.bind("<Key-Return>",self.doEnd)   

        self.eFactor = Entry()
        self.eFactor.pack()
        self.vFactor = StringVar()
        self.eFactor["textvariable"]=self.vFactor
        self.eFactor.bind("<Key-Return>",self.doFactor)

        self.eSteps = Entry()
        self.eSteps.pack()
        self.vSteps = StringVar()
        self.vSteps.set(str(self.steps))
        self.eSteps["textvariable"]=self.vSteps
        self.eSteps.bind("<Key-Return>",self.doSteps)

        self.eOrder = Entry(width=100)
        self.eOrder.pack()
        self.vOrder = StringVar()
        self.vOrder.set(self.orderStr)
        self.eOrder["textvariable"]=self.vOrder
        self.eOrder.bind("<Key-Return>",self.doOrder)

        self.player=[]

        self.setTempo(tempo)
        self.compute()

    def doOn(self):
        if self.vOn.get():
            self.play()
        else:
            self.stop()

    def doStart(self, event):
        try:
            if float(self.vStart.get())<0.0:
                self.vStart.set(str(self.initStart))
            if float(self.vEnd.get())<float(self.vStart.get()):
                self.vStart.set(self.vEnd.get())
        except:
            self.vStart.set(str(self.initStart))
        print "start: ", self.vStart.get()
        self.compute()

    def doEnd(self, event):
        try:
            if float(self.vEnd.get())>self.fileDur:
                self.vEnd.set(str(self.initEnd))
            if float(self.vEnd.get())<float(self.vStart.get()):
                self.vEnd.set(self.vStart.get())
        except:
            self.vEnd.set(str(self.initEnd))
        print "end: ", self.vEnd.get()
        self.compute()

    def doSteps(self, event):
        try:
            if int(self.vSteps.get())<1:
                self.vSteps.set(str(1))
        except:
                self.vSteps.set(str(1))
        self.steps= int(self.vSteps.get())
        print "steps: ", self.steps
        self.compute()

    def doOrder(self, event=[]):
        if event!=[]:
            self.stop()
        try:
            self.order=eval(self.vOrder.get())
            self.order=map(lambda x: int(round(x)),self.order)
            if any(map(lambda x:x<0 or x>self.steps-1,self.order)) or self.order==[]:
                raise Exception()
        except Exception as e:
            print self.vOrder.get()
            print e
            self.vOrder.set("range(self.steps)")
            self.order=eval(self.vOrder.get())
            
        print "order: ", self.order
        if event!=[]:
            self.play()

    #ex: to play just odd pieces, enter: self.shuffle(pieces=map(lambda x:2*x+1,range(64)))
    def shuffle(self,n=[],pieces=[]):
        if n==[]:
            n=self.steps
        if pieces==[]:
            pieces=range(n)
        for i in range(n-len(pieces)):
            pieces.append(pieces[i%len(pieces)])
        order=map(lambda x:x%self.steps,pieces)
        random.shuffle(order)
        return order

    def compute(self):
        self.stop()
        
        def findClosestInds(x,y):
            #assumes x and y are ascending lists
            
            out=[]
            done=False
            i=0
            j=0
            while not done and i<len(x):
                if x[i]>=y[j]:
                    if i==0 or x[i]-y[j]<y[j]-x[i-1]:
                        out.append(i)
                    else:
                        out.append(i-1)
                    j+=1
                    if j>=len(y):
                        done=True
                else:
                    i+=1
            while not done and j<len(y):
                out.append(len(x)-1)
                j+=1
            return out

        first=float(self.vStart.get())
        last=float(self.vEnd.get())
        
        breaks = map(lambda x:first+x*(last-first)/self.steps, range(self.steps)) + [last]
        breaks = map(lambda x:int(self.rate*x),breaks)        
        breakInds = findClosestInds(self.zeros,breaks)

        self.chunks=[]
        for i in range(self.steps):
            chunk=self.data[self.zeros[breakInds[i]]:self.zeros[breakInds[i+1]]]
            self.chunks.append(struct.pack("%sh" %len(chunk), *chunk))
        print "chunks computed"
        self.doOrder()
        self.play()

    def play(self):
        self.player=self.Player(self)
        self.playing=True
        self.player.start()
            
    def stop(self):
        if self.player!=[]:
            self.playing=False
            self.player.join()
            del self.player
            print "thread stopped"

    class Player(threading.Thread):
        def __init__(self,p):
            self.p=p
            self.ind=0
            threading.Thread.__init__(self)
            print "thread created"

        def run(self):
            print "thread started"
            while self.p.playing:
                self.p.stream.write(self.p.chunks[self.p.order[self.ind]])
                self.ind+=1
                if self.ind>=len(self.p.order):
                    self.ind=0
            
myapp = BigApp()
myapp.master.title("Looper")
myapp.master.lift()
myapp.mainloop()
